package edu.umb.cs.antmanager.manager;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.xmlbeans.XmlException;

import edu.umb.cs.antmanager.common.MachineStatus;
import edu.umb.cs.antmanager.common.ManagerInterface;
import edu.umb.cs.antmanager.domain.MachineType;
import edu.umb.cs.antmanager.domain.TestType;
import edu.umb.cs.antmanager.domain.TestsDocument;

import static edu.umb.cs.antmanager.common.Constants.*;

public class RMIManagerServer extends UnicastRemoteObject implements
		ManagerInterface {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static RMIManagerServer server;
	private ReentrantLock lock;
	private Condition idleMachineCondition;
	private HashMap<TestType, String> testsStatus;
	private HashMap<MachineType, String> machinesStatus;
	private ResourceManager resourceManager;
	private String managerHostName;

	public String getManagerHostName() {
		return managerHostName;
	}

	public static synchronized RMIManagerServer createRMIManager()
			throws RemoteException {
		if (server == null)
			server = new RMIManagerServer();
		return server;
	}

	private RMIManagerServer() throws RemoteException {
		super();
		lock = new ReentrantLock();
		idleMachineCondition = lock.newCondition();
		machinesStatus = new HashMap<MachineType, String>();
		testsStatus = new HashMap<TestType, String>();
		resourceManager = ResourceManager.getResourceManager();
		for (MachineType m : resourceManager.getMachines()){
			machinesStatus.put(m, null);
		}
		try{
			managerHostName = java.net.InetAddress.getLocalHost().getHostName();
		}catch(UnknownHostException e){
			//TODO cannot get host name
			System.err.println("cannot get manager's host name");
		}
	}

	private static void init() {
		System.setProperty("java.security.policy", SECURITY_FILE);
		System.setProperty("java.rmi.server.codebase", CODE_BASE);
		if (System.getSecurityManager() == null) {
			System.setSecurityManager(new SecurityManager());
		}

		try {
			LocateRegistry.createRegistry(SERVER_PORT);
		} catch (RemoteException e) {
			// There is a running Registry on that port, don't need
			// to create.
		}
	}

	public static void startServer() {
		init();
		try {
			Registry registry = LocateRegistry.getRegistry(SERVER_PORT);
			RMIManagerServer manager = RMIManagerServer.createRMIManager();
			registry.bind(MANAGER_SERVER_NAME, manager);
			manager.startTest();
		} catch (AccessException e) {
			// TODO the registry denied the binding request of the server
			System.err.println("Cannot bind the manage to the registry");
		} catch (RemoteException e) {
			// TODO the registry is not accessible
			System.err.println("The manager's registry isnot avaialable");
		} catch (AlreadyBoundException e) {
			// TODO There is another manager running on this machine
			System.err.println("You cannot run more than one managers on the same machine!");
			System.err.println("Please shutdwon the running manager first!");
			System.err.println("This window will close automatically in 10 seconds.");
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
			System.exit(0);
		}
	}

	public static void main(String[] args) {
		RMIManagerServer.startServer();
	}

	public void startTest() {
		lock.lock();
		try {
			TestsDocument testsDoc = TestsDocument.Factory.parse(new File(
					ANT_MANAGER_HOME+"\\"+"testList.xml"));
			for (TestType t : testsDoc.getTests().getTestArray())
				testsStatus.put(t, TEST_STATUS_UNTESTED);
			for(MachineType m : machinesStatus.keySet()){
				new Thread(new AgentConnRunnable(m.getName(),this,TASK_TYPE_STATUS,null)).start();
			}
			while (true) {
				if(isAllTestFinished()){
					System.out.println("All tests done!!");
					return;
				}
				for (MachineType m : getIdleMachineList()) {
					List<TestType> untestedTestList = getUntestedTestList();
					TestType t = resourceManager.findBestTest(untestedTestList,
							m.getResources());
					if (t != null) {
						new Thread(new AgentConnRunnable(m.getName(),this,TASK_TYPE_EXECUTE,t)).start();
						machinesStatus.put(m,STATUS_BUSY);
						testsStatus.put(t, TEST_STATUS_RUNNING);
					}
				}
				idleMachineCondition.await();
			}
		} catch (XmlException e) {
			// TODO testList.xml's format is incorrect
			System.err.println("Malformat testList.xml");
		} catch (IOException e) {
			// TODO testList.xml is not accessible
			System.err.println("TestList.xml is not found");
		} catch (InterruptedException e) {
			// TODO thread was interrupted
			System.out.println("Manager thread interrupted");
		} finally {
			lock.unlock();
		}
	}

	private boolean isAllTestFinished() {
		lock.lock();
		try {
			for (Map.Entry<TestType, String> e : testsStatus.entrySet()) {
				if (!e.getValue().equals(TEST_STATUS_FINISHED))
					return false;
			}
			return true;
		} finally {
			lock.unlock();
		}
	}

	private List<TestType> getUntestedTestList() {
		lock.lock();
		try {
			List<TestType> testList = new ArrayList<TestType>();
			for (Map.Entry<TestType, String> e : testsStatus.entrySet()) {
				if (e.getValue().equals(TEST_STATUS_UNTESTED))
					testList.add(e.getKey());
			}
			return testList;
		} finally {
			lock.unlock();
		}
	}

	private List<MachineType> getIdleMachineList() {
		lock.lock();
		try {
			List<MachineType> machineList = new ArrayList<MachineType>();
			for (Map.Entry<MachineType, String> e : machinesStatus.entrySet()) {
				if (e.getValue() != null && e.getValue().equals(STATUS_IDLE))
					machineList.add(e.getKey());
			}
			return machineList;
		} finally {
			lock.unlock();
		}
	}

	private MachineType getMachineByName(String machineName) {
		for (MachineType m : machinesStatus.keySet()) {
			if (m.getName().equals(machineName))
				return m;
		}
		return null;
	}

	private TestType getTestByName(String testName){
		for (TestType t : testsStatus.keySet()) {
			if(t.getName().equals(testName))
				return t;
		}
		return null;
	}
	@Override
	public void addTestLog(byte[] log, MachineStatus agentStatus)
			throws RemoteException {
		lock.lock();
		try {
			LogBuilder.getLogBuilder().addTestResult(
					new ByteArrayInputStream(log));
			TestType test = getTestByName(agentStatus.getRunningTest().getName());
			testsStatus.put(test, TEST_STATUS_FINISHED);
			updateMachineStatus(agentStatus.getMachineName(),STATUS_IDLE);
		} finally {
			lock.unlock();
		}

	}

	@Override
	public HashMap<MachineType, String> getMachinesStatus()
			throws RemoteException {
		return machinesStatus;
	}

	@Override
	public HashMap<TestType, String> getTestsStatus() throws RemoteException {
		return testsStatus;
	}

	@Override
	public void restartAgent(String agentName) throws RemoteException {
		new Thread(new AgentConnRunnable(agentName,this,TASK_TYPE_RESTART,null)).start();
	}

	@Override
	public void restartManager() throws RemoteException {
		shutDownManager();
		RMIManagerServer.startServer();
	}

	@Override
	public void shutDownAgent(String agentName) throws RemoteException {
		new Thread(new AgentConnRunnable(agentName,this,TASK_TYPE_SHUTDOWN,null)).start();
	}

	@Override
	public void shutDownManager() throws RemoteException {
		try {
			Registry registry = LocateRegistry.getRegistry();
			registry.unbind(MANAGER_SERVER_NAME);
			UnicastRemoteObject.unexportObject(server, true);
			server = null;
		} catch (NotBoundException e) {
			e.printStackTrace();
		}
	}

	public void updateMachineStatus(String agentName, String status) {
		lock.lock();
		try{
			machinesStatus.put(getMachineByName(agentName), status);
			if(status!=null && status.equals(STATUS_IDLE)){
				System.out.println("Notify idle Agent "+agentName);
				idleMachineCondition.signalAll();
			}
		}finally{
			lock.unlock();
		}
	}

}
